/*

This file is part of Rooms3D.

Copyright (C) 2008 Pixecur Technologies Incorporated (Esotera).
Visit Esotera at http://www.esoteras3.com
 
Rooms3D is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3, or (at your option)
any later version.
 
Rooms3D is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.
 
You should have received a copy of the GNU General Public License
along with Rooms3D; see the file COPYING.  If not, write to 
Pixecur Technologies, 213-31 Peet Street, St. John's, NL, A1B 3W8. 
 
Linking this library statically or dynamically with other modules is
making a combined work based on this library.  Thus, the terms and
conditions of the GNU General Public License cover the whole
combination.
 
*/

package com.datasentinel.rooms.file;


import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * @author rickp
 * 
 */
public class FolderMonitor implements Runnable
{
	private transient final static Logger logger = Logger.getLogger(FolderMonitor.class
			.getName());
	private static FolderMonitor instance;
	private final static Object mutex = new Object();
	private File folderToMonitor;
	private List<FolderChangeListener> listeners;
	private Map<File, Long> lastModTimes;
	private List<File> removed;
	private List<File> modified;
	private List<File> added;
	private static Thread folderCheckLoop;
	private IgnoreFileFilter ignoreFileFilter;
	private boolean cancel;
	private boolean pause;

	final public static synchronized FolderMonitor getInstance()
	{
		if (!(instance instanceof FolderMonitor))
		{
			instance = new FolderMonitor();
		}
		return instance;
	}

	private FolderMonitor()
	{
		super();
		lastModTimes = new Hashtable<File, Long>();
		listeners = Collections.synchronizedList(new ArrayList<FolderChangeListener>());
		removed = new ArrayList<File>();
		modified = new ArrayList<File>();
		added = new ArrayList<File>();
		ignoreFileFilter = new IgnoreFileFilter();
		cancel = true;
	}
	
	public void pause(boolean pause)
	{
		synchronized(mutex)
		{
			this.pause = pause;
			mutex.notify();
		}
	}
	
	private void _cancel(boolean notify)
	{
		synchronized (mutex)
		{
			// listeners.clear();
			cancel = true;
			if (notify)
				mutex.notify();
			if (folderCheckLoop != null && folderCheckLoop.isAlive())
				folderCheckLoop.interrupt();
		}
	}

	final public void setFolder(File newFolder) throws FolderMonitorException
	{
		if(newFolder == null) throw new FolderMonitorException("null argument not allowed");
		if(!newFolder.isDirectory()) throw new FolderMonitorException("argument must be a directory");
		_cancel(false);  // this performs folderCheckLoop.interrupt();
		synchronized (mutex)
		{
			logger.fine(String.format("starting thread to monitor folder [%s]",newFolder.getAbsolutePath()));
			lastModTimes.clear();
			folderToMonitor = newFolder;
			folderCheckLoop = new Thread(this,getClass().getSimpleName());
			folderCheckLoop.setDaemon(true);
			folderCheckLoop.start();
			mutex.notify();
		}
	}

	final public void addListener(FolderChangeListener toAdd)
	{
		synchronized (listeners)
		{
			if (!listeners.contains(toAdd))
			{
				listeners.add(toAdd);
				logger.fine(String.format("adding folder monitor listener [%s], num listeners=[%d]",toAdd,listeners.size()));
			}
			else
			{
				logger.info(String.format("ignoring add folder monitor request for previously added listener [%s], num listeners=[%d]",toAdd,listeners.size()));
			}
			
		}
	}

	final public void removeListener(FolderChangeListener toRemove)
	{
		synchronized (listeners)
		{
			if (listeners.contains(toRemove))
			{
				listeners.remove(toRemove);
				logger.fine(String.format("removing folder monitor listener [%s], num listeners=[%d]",toRemove,listeners.size()));
			}
			else
			{
				logger.info(String.format("ignoring folder monitor remove request, no such listener [%s], num listeners=[%d]",toRemove,listeners.size()));
			}
		}
	}

	final private void fireFilesRemoved(File[] removed)
	{
		synchronized (listeners)
		{
			FolderChangeListener[] listenerList = listeners.toArray(new FolderChangeListener[0]);
			for (FolderChangeListener listener : listenerList)
			{
				listener.filesRemoved(removed);
			}
		}
	}

	final private void fireFilesModified(File[] modified)
	{
		synchronized (listeners)
		{
			FolderChangeListener[] listenerList = listeners.toArray(new FolderChangeListener[0]);
			for (FolderChangeListener listener : listenerList)
			{
				listener.filesModified(modified);
			}
		}
	}

	final private void fireFilesAdded(File[] added)
	{
		synchronized (listeners)
		{
			FolderChangeListener[] listenerList = listeners.toArray(new FolderChangeListener[0]);
			for (FolderChangeListener listener : listenerList)
			{
				listener.filesAdded(added);
			}
		}
	}

	final public void preload(File file)
	{
		System.out.println("Preload: '" + file.getAbsolutePath() + "' " + file.hashCode());
		lastModTimes.put(file, file.lastModified());
	}
	
	final public void cancel()
	{
		_cancel(true);
	}
	
	final public void run()
	{
		logger.fine("started");
		cancel = false;
		while (!cancel)
		{
			try
			{
				synchronized (mutex)
				{
					if(pause)
					{
						logger.fine("thread paused");
						mutex.wait();
						logger.fine("thread no longer paused");
					}
					if(cancel)
					{
						logger.fine(String.format("folder monitor cancelled on [%s]", folderToMonitor!=null?folderToMonitor.getAbsolutePath():"null"));
						folderToMonitor = null;
						break;
					}
					if (folderToMonitor instanceof File)
					{
						Thread.currentThread().setName(getClass().getSimpleName()+" <"+folderToMonitor.getAbsolutePath()+"> ");
						mutex.wait(2000L);
					}
					else
					{
						logger.finer("thread waiting for folder to monitor...");
						mutex.wait();
						logger.finer("thread notified, wait completed.");
					}
				}
				synchronized (lastModTimes)
				{
					if (cancel)
						return;
					removed.clear();
					modified.clear();
					added.clear();
					
					if(folderToMonitor == null)
					{
						if(!cancel) logger.warning("no longer have a folder to monitor without cancel?");
						return;
					}
					
					File[] fileList = folderToMonitor.listFiles(ignoreFileFilter);
					List<File> folderFiles = Arrays
							.asList(fileList instanceof File[]?fileList:new File[] {});
					for (File key : lastModTimes.keySet())
					{
						if (!folderFiles.contains(key))
						{
							removed.add(key);
						}
					}
					for (File child : folderFiles)
					{
						if (!(child instanceof File))
						{
							logger.warning("File.listFiles() returned at a null child");
							continue;
						}
						if (lastModTimes.containsKey(child))
						{
							long storedModTime = lastModTimes.get(child);
							long actualModTime = child.lastModified();
							if (actualModTime > storedModTime)
							{
								modified.add(child);
								lastModTimes.put(child, actualModTime);
							}
						}
						else
						{
							added.add(child);
							lastModTimes.put(child, child.lastModified());
						}
					}
					try
					{
						if (!removed.isEmpty())
						{
							logger.finest("files removed: " + removed);
							lastModTimes.keySet().removeAll(removed);
							for (File file: added)
								System.out.println("Removed: '" + file.getAbsolutePath() + "' " + file.hashCode());
							fireFilesRemoved(removed.toArray(new File[removed.size()]));
						}
						if (!modified.isEmpty())
						{
							logger.finest("files modified: " + modified);
							fireFilesModified(modified.toArray(new File[modified.size()]));
						}
						if (!added.isEmpty())
						{
							logger.finest("files added:" + added);
							for (File file: added)
								System.out.println("Added: '" + file.getAbsolutePath() + "' " + file.hashCode());

							fireFilesAdded(added.toArray(new File[added.size()]));
						}
					}
					catch (Throwable e)
					{
						logger.log(Level.SEVERE, "Error processing folder change", e);
					}
				}
			}
			catch (InterruptedException ie)
			{
				logger.fine("interrupted, breaking from loop");
				return;
			}
			Thread.yield();
		}
		logger.fine("finished");
	}
}